/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package com.android.sdklib.internal.repository.archives;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.sdklib.repository.NoPreviewRevision;

import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ArchFilter {

    private static final String PROP_HOST_OS         = "Archive.HostOs";      //$NON-NLS-1$
    private static final String PROP_HOST_BITS       = "Archive.HostBits";    //$NON-NLS-1$
    private static final String PROP_JVM_BITS        = "Archive.JvmBits";     //$NON-NLS-1$
    private static final String PROP_MIN_JVM_VERSION = "Archive.MinJvmVers";  //$NON-NLS-1$

    /**
     * The legacy property used to serialize {@link LegacyOs} in source.properties files.
     * <p/>
     * Replaced by {@code ArchFilter.PROP_HOST_OS}.
     */
    public static final String LEGACY_PROP_OS   = "Archive.Os";       //$NON-NLS-1$

    /**
     * The legacy property used to serialize {@link LegacyArch} in source.properties files.
     * <p/>
     * Replaced by {@code ArchFilter.PROP_HOST_BITS} and {@code ArchFilter.PROP_JVM_BITS}.
     */
    public static final String LEGACY_PROP_ARCH = "Archive.Arch";     //$NON-NLS-1$

    private final HostOs mHostOs;
    private final BitSize mHostBits;
    private final BitSize mJvmBits;
    private final NoPreviewRevision mMinJvmVersion;

    /**
     * Creates a new {@link ArchFilter} with the specified filter attributes.
     * <p/>
     * This filters represents the attributes requires for a package's {@link Archive} to
     * be installable on the current architecture. Not all fields are required -- those that
     * are not specified imply there is no limitation on that particular attribute.
     *
     *
     * @param hostOs The host OS or null if there's no limitation for this package.
     * @param hostBits The host bit size or null if there's no limitation for this package.
     * @param jvmBits The JVM bit size or null if there's no limitation for this package.
     * @param minJvmVersion The minimal JVM version required by this package
     *                      or null if there's no limitation for this package.
     */
    public ArchFilter(@Nullable HostOs  hostOs,
                      @Nullable BitSize hostBits,
                      @Nullable BitSize jvmBits,
                      @Nullable NoPreviewRevision minJvmVersion) {
        mHostOs = hostOs;
        mHostBits = hostBits;
        mJvmBits = jvmBits;
        mMinJvmVersion = minJvmVersion;
    }

    /**
     * Creates an {@link ArchFilter} using properties previously saved in a {@link Properties}
     * object, typically by the {@link ArchFilter#saveProperties(Properties)} method.
     * <p/>
     * Missing properties are set to null and will not filter.
     *
     * @param props A properties object previously filled by {@link #saveProperties(Properties)}.
     *              If null, a default empty {@link ArchFilter} is created.
     */
    public ArchFilter(@Nullable Properties props) {
        HostOs  hostOs   = null;
        BitSize hostBits = null;
        BitSize jvmBits  = null;
        NoPreviewRevision minJvmVers = null;

        if (props != null) {
            hostOs   = HostOs .fromXmlName(props.getProperty(PROP_HOST_OS));
            hostBits = BitSize.fromXmlName(props.getProperty(PROP_HOST_BITS));
            jvmBits  = BitSize.fromXmlName(props.getProperty(PROP_JVM_BITS));

            try {
                minJvmVers = NoPreviewRevision.parseRevision(props.getProperty(PROP_MIN_JVM_VERSION));
            } catch (NumberFormatException ignore) {}

            // Backward compatibility with older PROP_OS and PROP_ARCH values
            if (!props.containsKey(PROP_HOST_OS) && props.containsKey(LEGACY_PROP_OS)) {
                hostOs = HostOs.fromXmlName(props.getProperty(LEGACY_PROP_OS));
            }
            if (!props.containsKey(PROP_HOST_BITS) &&
                    !props.containsKey(PROP_HOST_BITS) &&
                    props.containsKey(LEGACY_PROP_ARCH)) {
                // We'll only handle the typical x86 and x86_64 values of the old PROP_ARCH
                // value and ignore the PPC value. "Any" is equivalent to keeping the new
                // attributes to null.
                String v = props.getProperty(LEGACY_PROP_ARCH).toLowerCase();

                if (v.indexOf("x86_64") > 0) {
                    // JVM in 64-bit x86_64 mode so host-bits should be 64 too.
                    hostBits = jvmBits = BitSize._64;
                } else if (v.indexOf("x86") > 0) {
                    // JVM in 32-bit x86 mode, but host-bits could be either 32 or 64
                    // so we don't set this one.
                    jvmBits = BitSize._32;
                }
            }
        }

        mHostOs   = hostOs;
        mHostBits = hostBits;
        mJvmBits  = jvmBits;
        mMinJvmVersion = minJvmVers;
    }

    /** @return the host OS or null if there's no limitation for this package. */
    @Nullable
    public HostOs getHostOS() {
        return mHostOs;
    }

    /** @return the host bit size or null if there's no limitation for this package. */
    @Nullable
    public BitSize getHostBits() {
        return mHostBits;
    }

    /** @return the JVM bit size or null if there's no limitation for this package. */
    @Nullable
    public BitSize getJvmBits() {
        return mJvmBits;
    }

    /** @return the minimal JVM version required by this package
     *          or null if there's no limitation for this package. */
    @Nullable
    public NoPreviewRevision getMinJvmVersion() {
        return mMinJvmVersion;
    }

    /**
     * Checks whether {@code this} {@link ArchFilter} is compatible with the right-hand side one.
     * <p/>
     * Typically this is used to check whether "this downloaded package is compatible with the
     * current architecture", which would be expressed as:
     * <pre>
     * DownloadedArchive.filter.isCompatibleWith(ArhFilter.getCurrent())
     * </pre>
     * For the host OS & bit size attribute, if the attributes are non-null they must be equal.
     * For the min-jvm-version, "this" version (the package we want to install) needs to be lower
     * or equal to the "required" (current host) version.
     *
     * @param required The requirements to meet.
     * @return True if this filter meets or exceeds the given requirements.
     */
    public boolean isCompatibleWith(@NonNull ArchFilter required) {
        if (mHostOs != null
                && required.mHostOs != null
                && !mHostOs.equals(required.mHostOs)) {
            return false;
        }

        if (mHostBits != null
                && required.mHostBits != null
                && !mHostBits.equals(required.mHostBits)) {
            return false;
        }

        if (mJvmBits != null
                && required.mJvmBits != null
                && !mJvmBits.equals(required.mJvmBits)) {
            return false;
        }

        if (mMinJvmVersion != null
                && required.mMinJvmVersion != null
                && mMinJvmVersion.compareTo(required.mMinJvmVersion) > 0) {
            return false;
        }

        return true;
    }

    /**
     * Returns an {@link ArchFilter} that represents the current host platform.
     * @return an {@link ArchFilter} that represents the current host platform.
     */
    @NonNull
    public static ArchFilter getCurrent() {
        String os = System.getProperty("os.name");          //$NON-NLS-1$
        HostOs  hostOS = null;
        if (os.startsWith("Mac")) {                         //$NON-NLS-1$
            hostOS = HostOs.MACOSX;
        } else if (os.startsWith("Windows")) {              //$NON-NLS-1$
            hostOS = HostOs.WINDOWS;
        } else if (os.startsWith("Linux")) {                //$NON-NLS-1$
            hostOS = HostOs.LINUX;
        }

        BitSize jvmBits;
        String arch = System.getProperty("os.arch");        //$NON-NLS-1$

        if (arch.equalsIgnoreCase("x86_64")   ||            //$NON-NLS-1$
                arch.equalsIgnoreCase("ia64") ||            //$NON-NLS-1$
                arch.equalsIgnoreCase("amd64")) {           //$NON-NLS-1$
            jvmBits = BitSize._64;
        } else {
            jvmBits = BitSize._32;
        }

        // TODO figure out the host bit size.
        // When jvmBits is 64 we know it's surely 64
        // but that's not necessarily obvious when jvmBits is 32.
        BitSize hostBits = jvmBits;

        NoPreviewRevision minJvmVersion = null;
        String javav = System.getProperty("java.version");              //$NON-NLS-1$
        // java Version is typically in the form "1.2.3_45" and we just need to keep up to "1.2.3"
        // since our revision numbers are in 3-parts form (1.2.3).
        Pattern p = Pattern.compile("((\\d+)(\\.\\d+)?(\\.\\d+)?).*");  //$NON-NLS-1$
        Matcher m = p.matcher(javav);
        if (m.matches()) {
            minJvmVersion = NoPreviewRevision.parseRevision(m.group(1));
        }

        return new ArchFilter(hostOS, hostBits, jvmBits, minJvmVersion);
    }

    /**
     * Save this {@link ArchFilter} attributes into the the given {@link Properties} object.
     * These properties can later be given to the constructor that takes a {@link Properties} object.
     * <p/>
     * Null attributes are not saved in the properties.
     *
     * @param props A non-null properties object to fill with non-null attributes.
     */
    void saveProperties(@NonNull Properties props) {
        if (mHostOs != null) {
            props.setProperty(PROP_HOST_OS, mHostOs.getXmlName());
        }
        if (mHostBits != null) {
            props.setProperty(PROP_HOST_BITS, mHostBits.getXmlName());
        }
        if (mJvmBits != null) {
            props.setProperty(PROP_JVM_BITS, mJvmBits.getXmlName());
        }
        if (mMinJvmVersion != null) {
            props.setProperty(PROP_MIN_JVM_VERSION, mMinJvmVersion.toShortString());
        }
    }

    /** String for debug purposes. */
    @Override
    public String toString() {
        return "<ArchFilter mHostOs=" + mHostOs +
                ", mHostBits=" + mHostBits
                + ", mJvmBits=" + mJvmBits +
                ", mMinJvmVersion=" + mMinJvmVersion + ">";
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((mHostOs   == null) ? 0 : mHostOs.hashCode());
        result = prime * result + ((mHostBits == null) ? 0 : mHostBits.hashCode());
        result = prime * result + ((mJvmBits  == null) ? 0 : mJvmBits.hashCode());
        result = prime * result + ((mMinJvmVersion == null) ? 0 : mMinJvmVersion.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        ArchFilter other = (ArchFilter) obj;
        if (mHostBits != other.mHostBits) {
            return false;
        }
        if (mHostOs != other.mHostOs) {
            return false;
        }
        if (mJvmBits != other.mJvmBits) {
            return false;
        }
        if (mMinJvmVersion == null) {
            if (other.mMinJvmVersion != null) {
                return false;
            }
        } else if (!mMinJvmVersion.equals(other.mMinJvmVersion)) {
            return false;
        }
        return true;
    }

}
